package lumis.portal.serialization.operation;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import lumis.portal.PortalException;
import lumis.portal.serialization.DeserializationContext;
import lumis.portal.serialization.SerializationException;
import lumis.util.PortalUtil;
import lumis.util.TextUtil;

/**
 * Responsible for analyze foreign key dependencies of a portal object being deserialized.
 * @param <T> object being deserialized
 *
 * @version $Revision: 9307 $ $Date: 2008-05-16 15:21:14 -0300 (Fri, 16 May 2008) $
 * @since 4.0.9
 */
public class ConfigDependencyAnalyzer<T extends ICloneableConfig<T>>
{
	protected final DeserializationContext deserializationContext;
	protected final List<ConfigForeignKey> configForeignKeys;
	protected final T originalConfig;
	protected T persistableConfig;
	protected boolean hasViolation = false;
	protected final Collection<String> dependencyKeys = new ArrayList<String>();
	protected boolean canPersist = true;
	
	public ConfigDependencyAnalyzer(DeserializationContext deserializationContext, List<ConfigForeignKey> configForeignKeys, T originalConfig)
	{
		super();
		this.deserializationContext = deserializationContext;
		this.configForeignKeys = configForeignKeys;
		this.originalConfig = originalConfig;
		init();
	}
	private void init()
	{
		dependencyKeys.clear();
		hasViolation = false;
		canPersist = true;
		try
		{
			persistableConfig = originalConfig.clone();
		}
		catch (CloneNotSupportedException e)
		{
			throw new UnsupportedOperationException(e);
		}
	}
	public boolean getHasViolation()
	{
		return hasViolation;
	}
	public void setHasViolation(boolean hasViolation)
	{
		this.hasViolation = hasViolation;
	}
	public T getOriginalConfig()
	{
		return originalConfig;
	}
	/**
	 * Returns the persistable config, which represents a clone of original config, with its foreign keys updated to be non-violated. 
	 * @return persistable config    
	 */
	public T getPersistableConfig()
	{
		return persistableConfig;
	}
	/**
	 * Returns a mapped cloned of the persistable config, which, in turn, represents a clone of original config, with its foreign keys updated to be non-violated. 
	 * @return mapped cloned of the persistable config
	 */
	public T getMappedPersistableConfig()
	{
		T newPersistableConfig;
		try
		{
			newPersistableConfig = persistableConfig.clone();

			deserializationContext.applyForeignKeyFieldIdMappings(newPersistableConfig, configForeignKeys);

			return newPersistableConfig;
		}
		catch (CloneNotSupportedException e)
		{
			throw new UnsupportedOperationException(e);
		}
		catch (SerializationException e)
		{
			throw new IllegalStateException(e);
		}
	}

	public Collection<String> getDependencyKeys()
	{
		return dependencyKeys;
	}

	/**
	 * @return true, if the persistable config can be persisted.
	 */
	public boolean getCanPersist()
	{
		return canPersist;
	}

	/**
	 * @param foreignKeyName name of the config field that represents the foreign key
	 * @return value of the specified field in the original config. 
	 */
	private String getOriginalConfigForeignKeyValue(String foreignKeyName)
	{
		return getConfigForeignKeyValue(originalConfig, foreignKeyName);
	}

	/**
	 * @param foreignKeyName name of the config field that represents the foreign key
	 * @return value of the specified field in the persistable config. 
	 */
	private String getPersistableConfigForeignKeyValue(String foreignKeyName)
	{
		return getConfigForeignKeyValue(persistableConfig, foreignKeyName);
	}

	private String getConfigForeignKeyValue(T config, String foreignKeyName)
	{
		String value = null;
		Class targetClass = config.getClass();
		try
		{
			Field foreignKeyField = targetClass.getDeclaredField(foreignKeyName);
			foreignKeyField.setAccessible(true);
			value = (String)foreignKeyField.get(originalConfig); 
		}
		catch (Exception e)
		{
			throw new IllegalArgumentException("Error getting field '" + foreignKeyName + "' of class '" + targetClass.getName() + "'.", e);
		}
		return value;
	}

	private void setPersistableConfigForeignKeyValue(String foreignKeyName, String value)
	{
		Class targetClass = persistableConfig.getClass();
		try
		{
			Field foreignKeyField = targetClass.getDeclaredField(foreignKeyName);
			foreignKeyField.setAccessible(true);
			foreignKeyField.set(persistableConfig, value);
		}
		catch (Exception e)
		{
			throw new IllegalArgumentException("Error setting field '" + foreignKeyName + "' of class '" + targetClass.getName() + "'.", e);
		}
	}

	public void persist(IConfigPersister configPersister, boolean firstPersistenceIsRequired) throws PortalException
	{
		deserializationContext.logDebug("Persisting persistable config");
		analyze(configPersister);
		if (canPersist)
		{
			deserializationContext.logDebug("Persistable Config:", persistableConfig);
			if (!configPersister.persistConfig(deserializationContext, getMappedPersistableConfig(), true) && firstPersistenceIsRequired)
				throw new IllegalStateException("Error persisting deserialized config of type '" + persistableConfig.getClass().getName() + "'.");
		}
		else
		{
			deserializationContext.logDebug("Persistable Config won't be persisted");
		}
	}
	
	/**
	 * Analyze the original config, generating a persistable config and scheduling the persistence
	 * of original config to be performed after the dependencies were resolved. It also determines
	 * if the persistable config can be persisted.   
	 * @param configPersister
	 * @throws PortalException
	 */
	public void analyze(IConfigPersister configPersister)
	{
		deserializationContext.logDebug("Analyzing FKs");
		init();
		
		analyzeForeignKeys();

		if (configPersister != null && hasViolation)
			addDependentConfigOperation(configPersister);
	}
	
	/**
	 * Analyze the original config, generating a persistable config. It also determines
	 * if the persistable config can be persisted.   
	 */
	protected void analyzeForeignKeys()
	{
		T mappedPersistableConfig = getMappedPersistableConfig();
		for (ConfigForeignKey configForeignKey : configForeignKeys)
		{
			deserializationContext.logDebug("Analyzing field '" + configForeignKey.getName() + "': " + configForeignKey.toString());
			IConfigKeyChecker checker = configForeignKey.getChecker();
			
			if (checker != null)
			{
				String originalConfigForeignKeyValue = getOriginalConfigForeignKeyValue(configForeignKey.getName());
				if (originalConfigForeignKeyValue != null)
				{
					deserializationContext.logDebug(" Checking FK '" + originalConfigForeignKeyValue + "'");
					String mappedConfigForeignKeyValue = deserializationContext.getIdMapping(originalConfigForeignKeyValue);

					boolean foreignKeyViolated = false;
					// if it's creating new ids, and the foreign key value was not already mapped, consider it violated
					if (configForeignKey.isMutable() && deserializationContext.isCreateNewIds() 
							&& !deserializationContext.hasIdMapping(originalConfigForeignKeyValue) 
							&& !checker.keyExists(deserializationContext.getSessionConfig(), mappedConfigForeignKeyValue, null, deserializationContext.getTransaction()))
					{
						deserializationContext.logDebug(" Mutable field without mapping: considering key violated");
						foreignKeyViolated = true;
					}

					if (!foreignKeyViolated)
					{
						// check if the foreign key will be violated
						deserializationContext.logDebug(" Calling key checker");
						foreignKeyViolated = !checker.keyExists(deserializationContext.getSessionConfig(), mappedConfigForeignKeyValue, mappedPersistableConfig, deserializationContext.getTransaction());
					}
					if (foreignKeyViolated) // prepare for delayed persistence
					{
						hasViolation = true;
						if (!configForeignKey.getPersistIfViolated())
						{
							deserializationContext.logDebug(" Key violated: avoiding persistence");
							canPersist = false;
						}
						else
						{
							deserializationContext.logDebug(" Key violated: faking FK value");
							setPersistableConfigForeignKeyValue(configForeignKey.getName(), configForeignKey.getViolationValue());
						}
						if (configForeignKey.getAbortIfViolated()) // generate an unresolvable delayed persistence
						{
							deserializationContext.logDebug(" Key violated: adding an unresolvable dependency key");
							dependencyKeys.add(PortalUtil.generateNewGuid());
						}
						else
						{
							deserializationContext.logDebug(" Key violated: adding dependency key '" + originalConfigForeignKeyValue + "'");
							dependencyKeys.add(originalConfigForeignKeyValue);
						}
					}
					else
					{
						deserializationContext.logDebug(" Key not violated");
						setPersistableConfigForeignKeyValue(configForeignKey.getName(), mappedConfigForeignKeyValue);
					}
				}
				else
				{
					deserializationContext.logDebug(" Skipping checking null FK");
				}
			}			
		}
	}
	
	protected void addDependentConfigOperation(IConfigPersister configPersister)
	{
		ConfigPersistence configPersistence = getConfigPersistence(configPersister);
		deserializationContext.logDebug("Adding delayed persistence dependent on keys: " + TextUtil.joinStringArray(dependencyKeys.toArray(new String[0]), ",") + " Persister:" + (configPersistence.getConfigPersister() == null ? "null" : configPersistence.getConfigPersister().getClass().getName()), configPersistence.getConfig());
		deserializationContext.getDependentConfigPersistences().addDependentConfigOperation(dependencyKeys, configPersistence);
	}
	
	protected ConfigPersistence getConfigPersistence(IConfigPersister configPersister)
	{
		return new ConfigPersistence(originalConfig, configPersister);
	}
	
	/**
	 * Verify that all foreign key mapped values match persisted objects
	 * @return true, if all foreign key mapped values match persisted objects.
	 */
	public boolean foreignKeysExists()
	{
		T mappedPersistableConfig = getMappedPersistableConfig();
		for (ConfigForeignKey configForeignKey : configForeignKeys)
		{
			IConfigKeyChecker checker = configForeignKey.getChecker();
			if (checker != null)
			{
				String originalForeignKeyValue = getPersistableConfigForeignKeyValue(configForeignKey.getName());
				String mappedForeignKeyValue = deserializationContext.getIdMapping(originalForeignKeyValue);
				if (mappedForeignKeyValue != null && !checker.keyExists(deserializationContext.getSessionConfig(), mappedForeignKeyValue, mappedPersistableConfig, deserializationContext.getTransaction()))
					return false;
			}
		}
		return true;
	}
}
